/**
 * @author [(مۇختەرجان مەخمۇت) muhtaer mahmood]
 * @email yeganaaa@163.com
 * @create date 2019-10-15 21:36:15
 * @modify date 2019-10-15 21:36:15
 * @desc [description]
 */


import * as Net from "net"
import {RpcBridgeHost} from "rpc_dream";
import {PromiseCompleter} from "rpc_dream/bin/Rpc";

/**
 * Buffered socket wrapper
 * this is useful for writing and receiving data without split into multiple buffers(when send) and merge multiple buffers to single(when receive).
 * for receive data, you can assign [BufferedSocketWrapper.onDataReceived] callback property.
 */
export class BufferedSocketWrapper {
    private errorHandler: (error: Error) => void = error => { }
    private buffer: Buffer = null!
    private lengthOfTotalBytes: number = 0
    private lengthOfReceivedBytes: number = 0
    public onDataReceived: (data: Buffer) => void = data => { }
    public constructor(public socket: Net.Socket, public bufferSize: number) {
        socket.on("error", error => {
            this.errorHandler(error)
        })
        this.buffer = Buffer.allocUnsafe(this.bufferSize)
        const readBuffer = (buffer: Buffer) => {
            let valid = true
            valid = valid && this.lengthOfReceivedBytes == 0
            if (valid) {
                this.lengthOfTotalBytes = buffer.readUInt32LE(0)
                if (this.lengthOfTotalBytes > this.bufferSize) {
                    throw new Error(`This BufferedSocketWrapper buffer size is: '${this.bufferSize}', but size of the received data is: '${this.lengthOfTotalBytes}', size of received data exceed the buffer size.`)
                }
            }
            const sourceStart = this.lengthOfReceivedBytes == 0 ? 4 : 0/**because the first 4 byte of the data packet is the length of receiving data packet so this 4 bytes should ignore when receiving first piece(it's buffer in the here) of data packet*/
            const countOfShouldBeRead = this.lengthOfTotalBytes - this.lengthOfReceivedBytes
            this.lengthOfReceivedBytes += buffer.copy(this.buffer, this.lengthOfReceivedBytes, sourceStart, countOfShouldBeRead + sourceStart)
            valid = true
            valid = valid && this.lengthOfTotalBytes > 0
            valid = valid && this.lengthOfReceivedBytes == this.lengthOfTotalBytes
            if (valid) {
                valid = true
                valid = valid && countOfShouldBeRead < buffer.length - sourceStart
                this.onDataReceived(this.buffer.slice(0, this.lengthOfReceivedBytes))
                this.lengthOfTotalBytes = 0
                this.lengthOfReceivedBytes = 0
                if (valid)
                    readBuffer(buffer.slice(countOfShouldBeRead + sourceStart))
            }
        }
        socket.on("data", buffer => readBuffer(buffer))
        socket.on("close", () => {
            delete(this.buffer)
        })
    }
    public write(_buffer: Buffer): Promise<void> {
        const buffer = Buffer.allocUnsafe(_buffer.length + 4)
        const lengthOfBuffer = _buffer.length
        _buffer.copy(buffer, 4, 0, lengthOfBuffer)
        buffer.writeUInt32LE(lengthOfBuffer, 0)
        return new Promise<void>((resolve, reject) => {
            this.errorHandler = reject
            let hasWrited = this.socket.write(buffer)
            !hasWrited ? this.socket.once("drain", () => {
                resolve()
            }) : resolve()
        })
    }
}

export class TcpSocketRpcBridgeHost extends RpcBridgeHost<Net.Socket> {
    private wrapper: BufferedSocketWrapper = null!
    public _resolver: () => void = null!
    public shouldBeCompleteInitializationByExternally = false
    public constructor(private socketCreator: () => Promise<Net.Socket>, public bufferSize: number = 1024 * 128) {
        super()
    }
    socket: Net.Socket = null!
    initialize(): Promise<void> {
        return new Promise<void>(async (resolve, reject) => {
            this.socket = await this.socketCreator()
            this.wrapper = new BufferedSocketWrapper(this.socket, this.bufferSize)
            this.wrapper.onDataReceived = data => {
                this.onMessageReceived(data)
            }
            this.socket.on("ready", () => {
                if (this.shouldBeCompleteInitializationByExternally) return
                this.onConnected()
                resolve()
            })
            this.socket.on("error", error => {
                console.log("An Error Occurred.")
                console.error(error)
                reject(error)
            })
            this.socket.on("end", () => {
                // console.log("Connection ended.")
            })
            this.socket.on("close", () => {
                this.onClosed()
            })
            this.socket.on("timeout", () => {
                console.log("Connection is timeout.")
            })
            if (this.shouldBeCompleteInitializationByExternally) {
                this.onConnected()
                resolve()
            }
        })
    }

    close(callback: () => void): void {
        this.socket.end(callback)
    }
    send(data: Uint8Array, callback: (error: Error) => void): void {
        this.wrapper.write(Buffer.from(data)).then(() => {
            callback(null!)
        }).catch(error => {
            callback(error)
        })
    }
}
